/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using System.ComponentModel.Design;
using System.ComponentModel;
using System.Drawing.Design;

namespace DaveSexton.DocProject.Sandcastle.BuildComponents
{
	[TypeConverter(typeof(BuildComponents.SandcastleBuildComponentManagerTypeConverter))]
	[Editor(typeof(BuildComponents.SandcastleBuildComponentManagerEditor), typeof(UITypeEditor))]
	public sealed class SandcastleBuildComponentManager : MarshalByRefObject, IServiceProvider
	{
		#region Public Properties
		public bool LoadedFromFile
		{
			get
			{
				return loaded;
			}
		}

		public bool IsDirty
		{
			get
			{
				return dirty;
			}
			set
			{
				dirty = value;
			}
		}

		public string ConfigurationFile
		{
			get
			{
				return configFile;
			}
		}

		public string DisplayName
		{
			get
			{
				if (displayName != null)
					return displayName;
				else
					return parentComponent.SimpleTypeName;
			}
		}

		public string RootPropertyName
		{
			get
			{
				return rootPropertyName;
			}
		}

		public bool IsNested
		{
			get
			{
				return parentManager != null;
			}
		}

		public HelpTypes HelpType
		{
			get
			{
				return helpType;
			}
		}

		public SandcastleBuildComponentCollection Components
		{
			get
			{
				return componentsBuffer;
			}
		}

		public SandcastleProjectOptions Options
		{
			get
			{
				return options;
			}
		}

		public bool ShowEntireBuildComponentStack
		{
			get
			{
				if (parentManager == null)
					return options.ShowEntireBuildComponentStack;
				else
					// all sub components of a nested manager should be shown
					return true;
			}
		}
		#endregion

		#region Private / Protected
		private delegate SandcastleBuildComponent SandcastleBuildComponentCreator(int index, int lineNumber, string comment, string xml);

		/// <summary>
		/// X-path expression that matches all of the root build components in a Sandcastle configuration file for the Build Assembler utility; e.g., sandcastle.help1x.config.
		/// </summary>
		private const string componentsXPath = "/configuration/dduetools/builder/components";

		private readonly SandcastleBuildComponentCollection componentsBuffer, components;
		private readonly string rootPropertyName, configFile, displayName;
		private readonly SandcastleProjectOptions options;
		private readonly HelpTypes helpType;
		private XmlDocument document;
		/// <summary>
		/// A reference to the last node in the document that was loaded, even after the nodes are rearranged in memory.
		/// </summary>
		private XmlNode componentsNode, lastNode;
		private bool dirty, loaded;
		private readonly int lineOffset;
		private readonly SandcastleBuildComponent parentComponent;
		private readonly SandcastleBuildComponentManager parentManager;
		private readonly IXPathNavigable parentComponentConfig;
		private readonly Dictionary<XmlNode, string> configBuffer = new Dictionary<XmlNode, string>(64);
		private readonly Dictionary<int, XmlNode> nodeIndex = new Dictionary<int, XmlNode>(64);
		private readonly Dictionary<SandcastleBuildComponent, XmlNode> nodes = new Dictionary<SandcastleBuildComponent, XmlNode>(64);
		private readonly List<XmlNode> deleted = new List<XmlNode>(16);
		#endregion

		#region Constructors
		private SandcastleBuildComponentManager()
		{
			components = new SandcastleBuildComponentCollection(this);
			componentsBuffer = new SandcastleBuildComponentCollection(this);

			// This service is required for change notifcation of dynamic properties that are created using a TypeConverter on a build component.
			// (i.e., not dynamic build component properties themselves, but subproperties defined by the component author)
			// It's not required, however, for components that use a modal dialog or a drop down editor since the component itself is modified
			// directly through the SandcastleBuildComponentPropertyDescriptor class.
			IComponentChangeService change = (IComponentChangeService) DocProjectEnvironment.GetService(typeof(IComponentChangeService));

			if (change != null)
				change.ComponentChanged += componentChanged;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="SandcastleBuildComponentManager" /> class for a nested manager; 
		/// e.g., a manager that contains the sub components of an <strong>IfThenComponent</strong> or <strong>ForEachComponent</strong>.
		/// </summary>
		internal SandcastleBuildComponentManager(int lineOffset, SandcastleBuildComponent parentComponent, SandcastleBuildComponentManager parentManager, IXPathNavigable parentComponentConfig)
			: this()
		{
			if (parentComponent == null)
				throw new ArgumentNullException("parentComponent");

			if (parentManager == null)
				throw new ArgumentNullException("parentManager");

			if (parentComponentConfig == null)
				throw new ArgumentNullException("parentComponentConfig");

			this.rootPropertyName = parentManager.rootPropertyName;
			this.options = parentManager.options;
			this.configFile = parentManager.configFile;
			this.helpType = parentManager.helpType;

			this.parentComponent = parentComponent;
			this.parentManager = parentManager;
			this.lineOffset = lineOffset;
			this.parentComponentConfig = parentComponentConfig;

			// for nested managers, load the components immediately
			Load();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="SandcastleBuildComponentManager" /> class.
		/// </summary>
		internal SandcastleBuildComponentManager(string rootPropertyName, SandcastleProjectOptions options, string configFile, HelpTypes helpType)
			: this()
		{
			if (string.IsNullOrEmpty(rootPropertyName))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "rootPropertyName");

			if (options == null)
				throw new ArgumentNullException("options");

			if (string.IsNullOrEmpty(configFile))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "configFile");

			this.rootPropertyName = rootPropertyName;
			this.options = options;
			this.configFile = configFile;
			this.helpType = helpType;

			displayName = "{" + System.IO.Path.GetFileName(configFile) + "}";
		}
		#endregion

		#region Methods
		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		[System.Diagnostics.Conditional("DEBUG")]
		private void ValidateComponentCount()
		{
			System.Diagnostics.Debug.Assert(configBuffer.Count == nodeIndex.Count 
				&& nodeIndex.Count == componentsBuffer.Count,
				"Invalid component count.");
		}
		
		public void EnsureLoaded()
		{
			if (!loaded)
				Load();
		}

		public void Reload()
		{
			deleted.Clear();
			configBuffer.Clear();
			nodes.Clear();
			nodeIndex.Clear();
			components.Clear();
			componentsBuffer.Clear();

			dirty = false;
			loaded = false;

			Load();
		}

		private void Load()
		{
			if (!BuildComponentsLoader.Instance.TryLoadAssembly())
				throw new InvalidOperationException(Resources.Errors.SandcastleBuildComponentsLibraryNotFound);

			document = new XmlLineInfoDocument();
			document.PreserveWhitespace = true;

			if (parentManager == null)
				LoadFromFile();
			else
				LoadFromMemory();

			loaded = true;
			dirty = false;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void LoadFromMemory()
		{
			componentsNode = document.AppendChild(document.CreateElement("components"));
			componentsNode.InnerXml = parentComponentConfig.CreateNavigator().InnerXml;

			LoadComponents(delegate(int index, int lineNumber, string comment, string xml)
				{
					try
					{
						return new SandcastleBuildComponent(this, configFile, helpType, index, lineNumber, comment, xml);
					}
					catch (Exception ex)
					{
						return new SandcastleBuildComponent(this, configFile, helpType, ex, index, lineNumber, comment);
					}
				});
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void LoadFromFile()
		{
			document.Load(configFile);

			componentsNode = document.SelectSingleNode(componentsXPath);

			if (componentsNode != null)
				LoadComponents(delegate(int index, int lineNumber, string comment, string xml)
				{
					try
					{
						return new SandcastleBuildComponent(this, configFile, helpType, index, lineNumber, comment.ToString(), xml);
					}
					catch (Exception ex)
					{
						return new SandcastleBuildComponent(this, configFile, helpType, ex, index, lineNumber, comment.ToString());
					}
				});
				
		}

		private void LoadComponents(SandcastleBuildComponentCreator createComponent)
		{
			components.BeginInit();
			componentsBuffer.BeginInit();

			try
			{
				foreach (XmlElement node in componentsNode.SelectNodes("component"))
				{
					int lineNumber = ((IXmlLineInfo) node).LineNumber;
					int index = components.Count;
					StringBuilder comment = new StringBuilder();

					XmlNode previous = node.PreviousSibling;

					while (previous != null)
					{
						switch (previous.NodeType)
						{
							case XmlNodeType.Comment:
								comment.Insert(0, previous.InnerText + Environment.NewLine);
								previous = previous.PreviousSibling;
								break;
							case XmlNodeType.SignificantWhitespace:
							case XmlNodeType.Whitespace:
								previous = previous.PreviousSibling;
								break;
							default:
								previous = null;
								break;
						}
					}
					
					SandcastleBuildComponent component = createComponent(index, lineNumber + lineOffset, comment.ToString(), node.OuterXml);

					configBuffer.Add(node, null);
					nodes.Add(component, node);
					nodeIndex.Add(index, node);
					lastNode = node;

					components.Add(component);
					componentsBuffer.Add(component);
				}

				ValidateComponentCount();
			}
			finally
			{
				components.EndInit();
				componentsBuffer.EndInit();
			}
		}

		public void CommitChanges()
		{
			if (!dirty)
				return;

			CommitInternal();

			if (parentManager != null)
				// Save immediately since the parentManager handles buffering itself.  Nested components don't write 
				// to a file, they write to the parentManager, so SaveChanges isn't required for this purpose, although 
				// it serves other functions as well.
				CommitToParent(components);
		}

		private void CommitInternal()
		{
			System.Diagnostics.Debug.WriteLine("Committing changes to build component manager: " +
				rootPropertyName + ((parentComponent == null) ? "" : " (" + parentComponent.SimpleTypeName + ")"));

			foreach (XmlNode node in deleted)
				node.CreateNavigator().DeleteSelf();

			deleted.Clear();
			components.Clear();
			components.SuspendChangeTacking();

			try
			{
				foreach (SandcastleBuildComponent component in componentsBuffer)
				{
					component.CommitChanges();
					components.Add(component);
				}
			}
			finally
			{
				components.ResumeChangeTracking(true);
			}
		}

		private void CommitToParent(SandcastleBuildComponentCollection collection)
		{
			System.Diagnostics.Debug.WriteLine("Committing changes to a build component manager's parent component: " +
				rootPropertyName + " (" + parentComponent.SimpleTypeName + ")");

			StringBuilder output = new StringBuilder(1024);

			XmlWriterSettings settings = new XmlWriterSettings();
			settings.ConformanceLevel = ConformanceLevel.Fragment;

			using (XmlWriter writer = XmlWriter.Create(output, settings))
			{
				WriteComponents(collection, writer);
			}

			XmlNode node = (XmlNode) parentComponentConfig.CreateNavigator().UnderlyingObject;
			node.InnerXml = output.ToString();

			parentComponent.CommitChanges();
		}

		public void CancelChanges()
		{
			if (!dirty)
				return;

			System.Diagnostics.Debug.WriteLine("Canceling changes to build component manager: " +
				rootPropertyName + ((parentComponent == null) ? "" : " (" + parentComponent.SimpleTypeName + ")"));

			deleted.Clear();
			configBuffer.Clear();
			nodeIndex.Clear();
			componentsBuffer.Clear();

			componentsBuffer.SuspendChangeTacking();

			try
			{
				int index = 0;

				foreach (SandcastleBuildComponent component in components)
				{
					component.CancelChanges();
					componentsBuffer.Add(component);

					XmlNode node = nodes[component];

					component.Index = index;
					nodeIndex.Add(index, node);
					configBuffer.Add(node, null);

					index++;
				}

				ValidateComponentCount();
			}
			finally
			{
				componentsBuffer.ResumeChangeTracking(false);
			}

			if (parentComponent != null)
				parentComponent.CancelChanges();
		}

		public void SaveChanges()
		{
			if (dirty && document != null)
			{
				if (parentManager == null)
				// NOTE: Saving to disc only applies to root managers - i.e., if parentManager is not null then 
				// changes are saved directly to the parentManager during commits.
				{
					System.Diagnostics.Debug.WriteLine("Committing changes to build component manager: " + rootPropertyName);

					options.Project.GetItem(configFile).Checkout();

					using (XmlWriter writer = XmlWriter.Create(configFile))
					{
						writer.WriteStartDocument();

						using (XmlReader reader = document.CreateNavigator().ReadSubtree())
						{
							WriterStartConfiguration(reader, writer);

							WriteComponents(components, writer);

							WriteEndConfiguration(reader, writer);
						}
					}
				}

				foreach (SandcastleBuildComponent component in components)
					component.IsNew = false;
			}

			dirty = false;
		}

		private void WriteComponents(SandcastleBuildComponentCollection collection, XmlWriter writer)
		{
			string indent = new string('\t', 4);

			for (int i = 0; i < nodeIndex.Count; i++)
			{
				XmlNode node = nodeIndex[i];
				string xml = configBuffer[node];

				SandcastleBuildComponent.SaveConfiguration(node, xml);

				if (i > 0)
					writer.WriteWhitespace(Environment.NewLine + indent);

				string description = collection[i].CommentDescription;

				writer.WriteRaw(Environment.NewLine + indent);

				if (description != null && (description = description.Trim()).Length > 0)
				{
					writer.WriteComment(description);
					writer.WriteWhitespace(Environment.NewLine + indent);
				}

				writer.WriteNode(node.CreateNavigator(), false);

				if (i < nodeIndex.Count && node == lastNode)
					writer.WriteWhitespace(Environment.NewLine);
			}
		}

		internal SandcastleBuildComponent AddComponent(Type type, string assemblyPathOverride)
		{
			string assemblyPath = assemblyPathOverride ?? type.Assembly.CodeBase;

			Uri uri;
			if (Uri.TryCreate(assemblyPath, UriKind.RelativeOrAbsolute, out uri))
			{
				if (!uri.IsAbsoluteUri || (!uri.IsFile && !uri.IsUnc))
					assemblyPath = uri.ToString();
				else
					assemblyPath = uri.LocalPath;
			}

			XmlElement node = document.CreateElement("component");
			node.SetAttribute("type", type.FullName);
			node.SetAttribute("assembly", assemblyPath);
			node.InnerText = Environment.NewLine;

			componentsNode.AppendChild(node);

			int index = nodeIndex.Count;

			configBuffer[node] = null;
			nodeIndex.Add(index, node);

			SandcastleBuildComponent component = new SandcastleBuildComponent(this, configFile, helpType, index, type, node, true);
			
			componentsBuffer.Add(component);

			nodes.Add(component, node);

			ValidateComponentCount();

			dirty = true;

			return component;
		}

		private static void WriterStartConfiguration(XmlReader reader, XmlWriter writer)
		{
			while (reader.Read())
			{
				if (reader.IsStartElement("configuration")
					|| reader.IsStartElement("dduetools")
					|| reader.IsStartElement("builder"))
					writer.WriteStartElement(reader.LocalName);
				else if (reader.IsStartElement("components"))
				{
					writer.WriteStartElement("components");
					return;
				}
				else
					writer.WriteNode(reader, false);
			}
		}

		private static void WriteEndConfiguration(XmlReader reader, XmlWriter writer)
		{
			while (reader.Read() && reader.Depth > 3)
				// skip all components
				reader.Skip();

			if (!reader.EOF)
			{
				do
				{
					if (reader.NodeType == XmlNodeType.EndElement)
					{
						string name = reader.Name;

						if (string.Equals(name, "components", StringComparison.Ordinal)
							|| string.Equals(name, "builder", StringComparison.Ordinal)
							|| string.Equals(name, "dduetools", StringComparison.Ordinal)
							|| string.Equals(name, "configuration", StringComparison.Ordinal))
							writer.WriteEndElement();
						else
							writer.WriteNode(reader, false);
					}
					else
						writer.WriteNode(reader, false);
				}
				while (reader.Read());
			}

			writer.WriteEndDocument();
		}

		internal void MoveUp(SandcastleBuildComponent component)
		{
			int index = component.Index;
			int previousIndex = index - 1;

			if (nodeIndex.ContainsKey(previousIndex))
			{
				XmlNode node = nodeIndex[index];
				XmlNode previous = nodeIndex[previousIndex];

				nodeIndex[previousIndex] = node;
				nodeIndex[index] = previous;

				component.Index = previousIndex;
				componentsBuffer[previousIndex].Index = index;

				componentsBuffer.MoveUp(component);

				ValidateComponentCount();

				dirty = true;
			}
		}

		internal void MoveDown(SandcastleBuildComponent component)
		{
			int index = component.Index;
			int nextIndex = index + 1;

			if (nodeIndex.ContainsKey(nextIndex))
			{
				XmlNode node = nodeIndex[index];
				XmlNode next = nodeIndex[nextIndex];

				nodeIndex[nextIndex] = node;
				nodeIndex[index] = next;

				component.Index = nextIndex;
				componentsBuffer[nextIndex].Index = index;

				componentsBuffer.MoveDown(component);

				ValidateComponentCount();

				dirty = true;
			}
		}

		internal void Remove(SandcastleBuildComponent component)
		{
			int index = component.Index;

			XmlNode node = nodeIndex[index];

			deleted.Add(node);

			configBuffer.Remove(node);
			componentsBuffer.Remove(component);

			for (int i = index; i < componentsBuffer.Count; i++)
			{
				componentsBuffer[i].Index = i;
				nodeIndex[i] = nodeIndex[i + 1];
			}

			nodeIndex.Remove(componentsBuffer.Count);

			ValidateComponentCount();

			dirty = true;
		}

		internal void NotifyChanged(bool notifyParent)
		{
			foreach (SandcastleBuildComponent component in components)
				NotifyChanged(component, false);

			if (notifyParent)
				NotifyParentOfChange();
		}

		internal void NotifyChanged(SandcastleBuildComponent component, bool notifyParent)
		{
			dirty = true;
			configBuffer[nodeIndex[component.Index]] = component.ConfigurationAsString;

			ValidateComponentCount();

			if (notifyParent)
				// Normally the parent must be notified of changes, unless the change occurred within a dialog 
				// that supports cancelation, in which case the components have not yet been committed and so
				// the parent shouldn't be notified.
				NotifyParentOfChange();
		}

		private void NotifyParentOfChange()
		{
			if (parentManager != null)
			{
				// Save immediately since the parentManager handles buffering itself.  Nested components don't write 
				// to a file, they write to the parentManager; SaveChanges isn't required for this purpose, although 
				// it serves other functions as well.
				CommitChanges();

				parentManager.NotifyChanged(parentComponent, true);
			}

			options.NotifyChanged(rootPropertyName, this, this);
		}

		internal void NotifyChanging()
		{
			if (parentManager != null)
				parentManager.NotifyChanging();
			else
				options.NotifyChanging(rootPropertyName);
		}

		public override string ToString()
		{
			if (displayName != null)
				return displayName;
			else
				// display the number of components for a nested manager (root managers always have a displayName set to the config file name)
				return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0} component(s)", components.Count);
		}
		#endregion

		#region Event Handlers
		/// <remarks>
		/// The term component is overloaded; <c>e.Component</c> refers to the <em>changed</em> object, which
		/// could be the value of any property in a property grid.  However, this event handler is only meant
		/// to handle changes to Sandcastle build component when they are represented as properties in a 
		/// property grid.
		/// </remarks>
		private void componentChanged(object sender, ComponentChangedEventArgs e)
		{
			SandcastleBuildComponent component = e.Component as SandcastleBuildComponent;

			if (component != null && componentsBuffer.Contains(component))
				NotifyChanged(component, true);
		}
		#endregion

		#region Nested
		private sealed class XmlLineInfoDocument : XmlDocument
		{
			#region Public Properties
			#endregion

			#region Private / Protected
			private XmlReader reader;
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="XmlLineInfoDocument" /> class.
			/// </summary>
			public XmlLineInfoDocument()
			{
			}
			#endregion

			#region Methods
			public override void Load(XmlReader docReader)
			{
				this.reader = docReader;
				base.Load(docReader);
			}

			public override XmlElement CreateElement(string prefix, string localName, string namespaceURI)
			{
				return new XmlLineInfoElement(prefix, localName, namespaceURI, this, (IXmlLineInfo) reader);
			}
			#endregion

			#region Nested
			private sealed class XmlLineInfoElement : XmlElement, IXmlLineInfo
			{
				#region Private / Protected
				private readonly int lineNumber, linePosition;
				#endregion

				#region Constructors
				/// <summary>
				/// Constructs a new instance of the <see cref="XmlLineInfoElement" /> class.
				/// </summary>
				public XmlLineInfoElement(string prefix, string localName, string namespaceURI,
					XmlDocument document, IXmlLineInfo lineInfo)
					: base(prefix, localName, namespaceURI, document)
				{
					if (lineInfo != null && lineInfo.HasLineInfo())
					{
						lineNumber = lineInfo.LineNumber;
						linePosition = lineInfo.LinePosition;
					}
				}
				#endregion

				#region IXmlLineInfo Members
				bool IXmlLineInfo.HasLineInfo()
				{
					return true;
				}

				public int LineNumber
				{
					get
					{
						return lineNumber;
					}
				}

				public int LinePosition
				{
					get
					{
						return linePosition;
					}
				}
				#endregion
			}
			#endregion
		}
		#endregion

		#region IServiceProvider Members
		public object GetService(Type serviceType)
		{
			return options.GetService(serviceType, null);
		}
		#endregion
	}
}
